Managing UI State with @State, @Binding, and Observable Objects in SwiftUI
Course: iOS App Development
Topic: Managing UI State with @State, @Binding, and Observable Objects
Course Link: iOS App Development Training at JustAcademy
Demo Link: Register for a Free Course Demo
Official Website: JustAcademy
1. Introduction to UI State Management
UI state management is the process of storing, updating, and sharing the data that determines what an application displays on the screen. In iOS development, an interface may change when a user taps a button, enters text, selects an option, opens a menu, or receives updated information from a service.
SwiftUI provides a declarative approach to building interfaces. Developers describe how the interface should appear for a particular state, and SwiftUI updates the relevant views when the data changes.
The three important concepts covered in this topic are @State, @Binding, and observable objects. They help developers organize data, share values between views, and keep application interfaces synchronized with changing information.
Learning Objectives
- Understand UI state and reactive interface updates.
- Learn how to declare and manage local state using @State.
- Understand how @Binding shares editable state between views.
- Learn how observable objects communicate changes to SwiftUI.
- Understand @Observable, ObservableObject, @StateObject, and @ObservedObject.
- Build reusable views that share application data.
- Apply state management concepts in practical iOS applications.
- Identify common state management mistakes and debugging techniques.
2. What Is UI State?
UI state represents the current condition of an application's interface. It includes information such as whether a button is selected, whether a dialog is visible, what text a user has entered, and which items appear in a list.
Examples of UI State
| State Variable |
Data Type |
Purpose |
| isLoggedIn |
Bool |
Tracks whether a user is logged in. |
| counter |
Int |
Stores a numeric counter value. |
| username |
String |
Stores text entered by the user. |
| isDarkMode |
Bool |
Tracks a theme preference. |
| selectedTab |
Int |
Identifies the currently selected tab. |
| isShowingAlert |
Bool |
Controls whether an alert appears. |
| items |
Array |
Stores a collection displayed in a list. |
How State Changes Affect the Interface
- The application stores a value.
- A SwiftUI view reads that value to determine its appearance.
- The user or application changes the value.
- SwiftUI detects the relevant state change.
- The affected parts of the interface are updated.
This approach reduces the need to manually refresh interface elements after every data change.
3. Understanding @State
@State is a SwiftUI property wrapper used to store a value that belongs to a view and its local interface behavior. SwiftUI manages the storage and updates dependent views when the value changes.
It is suitable for simple values such as integers, strings, Boolean flags, and arrays that a view needs to modify.
Key Characteristics of @State
- Stores state owned by a SwiftUI view.
- Supports mutable values.
- Allows the interface to respond to changes automatically.
- Is commonly declared with the private access modifier.
- Can provide a binding to its value through the dollar-sign syntax.
- Works best when the state belongs to the view and its subviews rather than a shared application-wide model.
Example: Counter Using @State
import SwiftUI
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text("Count: \(count)")
.font(.largeTitle)
Button("Increase Count") {
count += 1
}
Button("Reset Count") {
count = 0
}
}
.padding()
}
}
Code Explanation
import SwiftUI imports the SwiftUI framework.
CounterView defines a view that displays a counter.
@State private var count = 0 creates a state value initialized to zero.
Text("Count: \(count)") displays the current counter value.
count += 1 increases the counter when the button is tapped.
count = 0 resets the counter.
Whenever the counter changes, SwiftUI recalculates the relevant view body and displays the new value.
4. Using @State with Text Fields
SwiftUI's TextField needs access to editable text. A state variable can store the text, while a binding connects the TextField to that variable.
Example: Username Input
import SwiftUI
struct UsernameView: View {
@State private var username = ""
var body: some View {
VStack(spacing: 16) {
TextField("Enter your username", text: $username)
.textFieldStyle(.roundedBorder)
Text("Hello, \(username.isEmpty ? "Guest" : username)!")
}
.padding()
}
}
Explanation
username stores the entered text.
$username provides a Binding to the state value.
TextField reads and writes the username through the binding.
- The greeting updates when the user enters or deletes text.
The dollar sign is important because TextField requires a binding for its editable text value rather than a plain String.
5. Understanding @Binding
@Binding creates a two-way connection to a value owned elsewhere. It allows a child view to read and modify data without owning the original state.
For example, a parent view might own a Boolean value that determines whether a setting is enabled. A reusable child view can receive a binding to that value and allow the user to change it.
Key Characteristics of @Binding
- Does not create independent storage for the underlying value.
- Connects a view to state owned by another component.
- Allows changes made by a child view to update the original value.
- Uses the dollar-sign syntax when passing a binding from a state property.
- Helps create reusable and modular SwiftUI components.
Example: Parent and Child Views
import SwiftUI
struct ParentView: View {
@State private var isEnabled = false
var body: some View {
VStack(spacing: 16) {
Text(isEnabled ? "Setting Enabled" : "Setting Disabled")
SettingToggleView(isEnabled: $isEnabled)
}
.padding()
}
}
struct SettingToggleView: View {
@Binding var isEnabled: Bool
var body: some View {
Toggle("Enable Setting", isOn: $isEnabled)
}
}
How It Works
- The parent view creates and owns the
isEnabled state.
- The parent passes
$isEnabled to the child view.
- The child receives the binding through
@Binding.
- The Toggle reads the current value and allows the user to change it.
- The original state changes, and the parent's text updates automatically.
6. Difference Between @State and @Binding
| Feature |
@State |
@Binding |
| Primary purpose |
Owns view-local state. |
Accesses state owned elsewhere. |
| Storage |
SwiftUI manages the state storage. |
Does not own the underlying value. |
| Typical location |
Parent or owning view. |
Child or reusable subview. |
| Declaration |
@State private var count = 0 |
@Binding var count: Int |
| Passing data |
Pass the value for read-only use or a binding for editing. |
Receives a binding from another source of truth. |
| Example |
Owns a selected option. |
Allows a child picker to change that option. |
7. Understanding Observable Objects
When an application grows, state may need to be shared by several views. Instead of placing every property inside one view, developers can create a model object that manages related data and business logic.
In SwiftUI, observable models allow views to respond to changes in model properties. There are two important approaches:
- Modern Observation: Use the
@Observable macro, available on supported Apple platforms starting with iOS 17 and corresponding platform releases.
- Combine-based observation: Use the
ObservableObject protocol with @Published properties and the appropriate SwiftUI property wrappers.
These approaches are related but use different observation mechanisms. Choose the approach that fits your deployment target and project architecture.
8. Modern Observable Models Using @Observable
The @Observable macro makes a model's stored properties observable. SwiftUI can track the properties a view reads and update dependent views when those properties change.
This approach is available in iOS 17 and later, as well as corresponding releases of other Apple operating systems.
Example: User Profile Model
import SwiftUI
import Observation
@Observable
class UserProfile {
var name = "Alex"
var age = 25
var isPremium = false
}
Explanation
@Observable adds observation support to the class.
name stores the user's name.
age stores the user's age.
isPremium represents a Boolean account setting.
- Views that read these observable properties can update when the properties change.
9. Managing an Observable Model with @State
For modern Observation-based models, a SwiftUI view can store the model instance using @State. SwiftUI manages the model reference and tracks observable properties read by the view.
Example: Profile Management Screen
import SwiftUI
import Observation
@Observable
class UserProfile {
var name = "Alex"
var age = 25
var isPremium = false
}
struct ProfileView: View {
@State private var profile = UserProfile()
var body: some View {
VStack(spacing: 16) {
Text("Name: \(profile.name)")
Text("Age: \(profile.age)")
Toggle("Premium Membership", isOn: $profile.isPremium)
Button("Change Name") {
profile.name = "Jordan"
}
}
.padding()
}
}
In this example, the view owns the observable model. The displayed name and age react to model changes, and the Toggle receives a binding to the observable model's Boolean property.
10. Sharing Observable Models Between Views
An observable model can be passed to multiple views so that each view can display or modify the data it needs. The model provides a shared source of truth without requiring every property to be copied into separate state variables.
Example: Parent and Child Views with @Observable
import SwiftUI
import Observation
@Observable
class TaskModel {
var taskName = "Learn SwiftUI"
var isCompleted = false
}
struct TaskParentView: View {
@State private var task = TaskModel()
var body: some View {
VStack(spacing: 16) {
Text("Task: \(task.taskName)")
TaskStatusView(task: task)
}
.padding()
}
}
struct TaskStatusView: View {
var task: TaskModel
var body: some View {
VStack(spacing: 12) {
Text(task.isCompleted ? "Completed" : "In Progress")
Button(task.isCompleted ? "Mark Incomplete" : "Complete Task") {
task.isCompleted.toggle()
}
}
}
}
The parent creates and owns the model. The child receives the same reference, so its changes affect the shared model. Because the child reads task.isCompleted, SwiftUI can update the dependent interface when that property changes.
11. Using @Bindable with Observable Models
When a child view needs to provide editable controls for properties of an @Observable model, @Bindable can create bindings to those properties.
Example: Editing a User Profile
import SwiftUI
import Observation
@Observable
class UserSettings {
var username = "Alex"
var notificationsEnabled = true
}
struct SettingsEditorView: View {
@Bindable var settings: UserSettings
var body: some View {
Form {
TextField("Username", text: $settings.username)
Toggle(
"Enable Notifications",
isOn: $settings.notificationsEnabled
)
}
}
}
struct SettingsParentView: View {
@State private var settings = UserSettings()
var body: some View {
SettingsEditorView(settings: settings)
}
}
Important distinction: @Binding connects a view to an existing value or binding, while @Bindable creates bindings to mutable properties of an Observation-based model. The model itself remains owned by its source of truth.
12. Using ObservableObject with @Published
Before the modern Observation approach, SwiftUI commonly used the Combine-based ObservableObject protocol. This remains useful for applications that support earlier operating system versions or already use this architecture.
The @Published property wrapper announces changes to a property so that observing SwiftUI views can update when the model changes.
Example: ObservableObject Model
import SwiftUI
import Combine
class CounterModel: ObservableObject {
@Published var count = 0
func increment() {
count += 1
}
func reset() {
count = 0
}
}
Explanation
ObservableObject defines the model's observable-object behavior.
@Published publishes changes to the counter property.
increment() increases the counter.
reset() resets the counter to zero.
13. Understanding @StateObject and @ObservedObject
When working with Combine-based observable objects, SwiftUI provides property wrappers for managing ownership and observing changes.
@StateObject
@StateObject creates and manages an observable object instance for the lifetime of the owning view's identity. Use it when a view is responsible for creating and owning the model.
@ObservedObject
@ObservedObject observes an observable object supplied to a view, commonly by a parent. It does not establish ownership in the same way as @StateObject.
Example: Parent and Child with ObservableObject
import SwiftUI
import Combine
class CounterModel: ObservableObject {
@Published var count = 0
}
struct CounterParentView: View {
@StateObject private var model = CounterModel()
var body: some View {
VStack(spacing: 16) {
Text("Count: \(model.count)")
CounterChildView(model: model)
}
.padding()
}
}
struct CounterChildView: View {
@ObservedObject var model: CounterModel
var body: some View {
Button("Increase") {
model.count += 1
}
}
}
The parent creates and owns the model using @StateObject. The child observes the same model through @ObservedObject. When the published count changes, both views can update because they depend on that model value.
14. Comparing Modern Observation and ObservableObject
| Feature |
@Observable Approach |
ObservableObject Approach |
| Observation mechanism |
Swift Observation framework. |
Combine-based object publishing. |
| Model declaration |
@Observable class Model |
class Model: ObservableObject |
| Property annotation |
Ordinary stored properties can be tracked. |
Typically uses @Published. |
| Owning view |
Often uses @State. |
Often uses @StateObject. |
| Receiving view |
Can receive the model as a normal property. |
Often uses @ObservedObject. |
| Editable property bindings |
Use @Bindable where appropriate. |
Can use bindings projected from @ObservedObject. |
| Platform considerations |
Available on iOS 17 and later. |
Suitable for older deployment targets that support SwiftUI and Combine. |
15. Choosing the Correct State Management Tool
Select a state management tool based on who owns the data, how many views need it, and whether a child needs permission to modify it.
| Requirement |
Recommended Tool |
Reason |
| Store a local counter or Boolean value. |
@State |
The value belongs to the view. |
| Allow a child view to edit a parent's value. |
@Binding |
The child receives a connection to existing state. |
| Share a modern observable model. |
@Observable with @State |
The owning view manages the model and dependent views track properties they read. |
| Edit properties on a modern observable model. |
@Bindable |
Provides bindings to model properties. |
| Create a Combine-based observable model. |
@StateObject |
The view owns the model instance. |
| Observe a Combine-based model provided by a parent. |
@ObservedObject |
The receiving view reacts to published changes. |
16. Practical Project: Task Management Application
Project Objective: Build a small task management interface using SwiftUI state and observable data.
Project Features
- Display a task title.
- Allow users to mark a task as completed.
- Update the interface when the task status changes.
- Use a reusable child view for task controls.
- Provide a button to reset the task status.
Swift Implementation
import SwiftUI
import Observation
@Observable
class Task {
var title = "Complete SwiftUI Practice"
var isCompleted = false
func toggleCompletion() {
isCompleted.toggle()
}
func reset() {
isCompleted = false
}
}
struct TaskView: View {
@State private var task = Task()
var body: some View {
VStack(spacing: 20) {
Text(task.title)
.font(.title2)
Text(task.isCompleted ? "Task Completed" : "Task Pending")
.foregroundStyle(
task.isCompleted ? .green : .secondary
)
TaskControls(task: task)
Button("Reset Task") {
task.reset()
}
}
.padding()
}
}
struct TaskControls: View {
var task: Task
var body: some View {
Button(
task.isCompleted ? "Mark Pending" : "Mark Completed"
) {
task.toggleCompletion()
}
}
}
Project Explanation
- The Task model stores the title and completion status.
- The
@Observable macro enables observation of model properties.
- The parent view owns the model using
@State.
- The child view receives the same model instance.
- The user can toggle the task status.
- The parent displays the current status based on the shared model.
- The Reset Task button sets the completion state to false.
This project demonstrates shared observable data without requiring a separate binding for every model property. The child modifies the shared model, and views update when they depend on changed properties.
17. Common Mistakes and Troubleshooting
| Problem |
Possible Cause |
Solution |
| View does not update after a value changes. |
The value is not stored or observed correctly. |
Check the state declaration and how the view reads the value. |
| Cannot pass a String to a TextField binding. |
A plain String was passed instead of a Binding. |
Pass the projected state value, such as $username. |
| Child changes do not affect parent state. |
The child stores a separate local value. |
Use @Binding when the child needs to edit parent-owned state. |
| ObservableObject changes do not refresh the view. |
A property is not published or the object is not observed correctly. |
Check @Published and the appropriate observing wrapper. |
| Model instance is recreated unexpectedly. |
The model's ownership or view identity is incorrect. |
Review the owning view and use the appropriate state mechanism. |
| Compiler error involving @ObservedObject. |
A modern Observation model is being used with a Combine-based wrapper. |
Use the appropriate Observation APIs for the model type. |
| Changes do not persist after restarting the app. |
UI state is stored only in memory. |
Use suitable persistence such as SwiftData, files, or a backend service. |
| Two views show inconsistent values. |
Each view may own a separate copy of the data. |
Share a common source of truth. |
Debugging Best Practices
- Identify which view or model owns each value.
- Avoid creating duplicate state for the same piece of information.
- Use @Binding only when a view needs a binding to existing state.
- Choose the correct observation system for the model type.
- Keep business logic out of large view bodies where practical.
- Use clear names for state variables and model properties.
- Test changes from both parent and child views.
- Remember that UI state in memory is not automatically permanent storage.
18. Best Practices for SwiftUI State Management
- Maintain a single source of truth: Avoid storing multiple independent copies of the same value.
- Keep local state local: Use @State for data that belongs to a view and its subviews.
- Share editable values through bindings: Use @Binding when a child must modify state owned by another view.
- Separate models from presentation: Use observable models to organize shared application data and logic.
- Select the right observation approach: Use @Observable for modern Observation-based models or ObservableObject when appropriate.
- Keep views reusable: Pass only the values or model references that a child needs.
- Use appropriate persistence: Store data separately when it must survive view recreation or application restarts.
- Test state transitions: Verify loading, success, failure, empty, and completed states.
19. Interview Questions and Answers
Q1. What is UI state in SwiftUI?
UI state is the data that determines the current appearance and behavior of an interface.
Q2. What is @State?
@State is a property wrapper that allows SwiftUI to manage a value owned by a view and update dependent interface content when the value changes.
Q3. What is @Binding?
@Binding provides a two-way connection to a value owned elsewhere, allowing a child view to read and modify that value.
Q4. What is the difference between @State and @Binding?
@State manages view-owned storage, whereas @Binding provides access to existing storage owned by another component.
Q5. What is an observable object?
An observable object is a model whose changes can be observed by SwiftUI views so that relevant interface content can update.
Q6. What is @Observable?
@Observable is a macro from the Observation framework that adds observation support to a model's properties.
Q7. What is ObservableObject?
ObservableObject is a Combine-based protocol used by objects that publish changes for observing views.
Q8. What is the purpose of @Published?
@Published publishes changes to a property in a Combine-based observable object.
Q9. What is the difference between @StateObject and @ObservedObject?
@StateObject manages an observable object owned by a view, while @ObservedObject observes an object supplied to a view.
Q10. When should @Bindable be used?
@Bindable is useful when a view needs bindings to mutable properties of an @Observable model, such as when connecting model properties to TextField or Toggle.
Q11. Why does SwiftUI automatically update views?
SwiftUI tracks dependencies between views and their data. When relevant state or observable properties change, SwiftUI updates affected portions of the interface.
Q12. Does @State automatically save data permanently?
No. @State manages view-related state in memory. Permanent storage requires an appropriate persistence mechanism.
Q13. Can multiple views share the same observable model?
Yes. Multiple views can receive the same model instance and react to the observable properties they read.
Q14. Which observation approach is available on iOS 17 and later?
The modern Observation framework, including the @Observable macro, is available on iOS 17 and later.
20. Quick Revision Checklist
- @State stores state owned by a view.
- @Binding connects a view to state owned elsewhere.
- The dollar sign provides a binding from suitable state properties.
- @Observable enables modern property observation.
- @Bindable creates bindings to mutable properties of observable models.
- ObservableObject supports the Combine-based observation approach.
- @Published announces changes to properties of ObservableObject models.
- @StateObject manages an owned ObservableObject instance.
- @ObservedObject observes an ObservableObject supplied to a view.
- A single source of truth reduces inconsistent application state.
- State in memory is not automatically persisted across application restarts.
21. Conclusion
Managing UI state is a fundamental skill in SwiftUI development. @State is appropriate for values owned by a view, @Binding allows child views to modify existing state, and observable models help organize data shared across multiple parts of an application.
Developers can use the modern @Observable approach or the Combine-based ObservableObject approach according to their deployment requirements. Understanding ownership, data flow, and observation helps create responsive, reusable, and maintainable iOS applications.
Practice these concepts by developing counters, settings screens, profile editors, and task management applications. These projects provide a strong foundation for advanced SwiftUI development, including navigation, persistence, networking, and application architecture.
Learn More with JustAcademy
Explore iOS app development training and strengthen your practical knowledge of Swift, SwiftUI, and native application development.
Course Details: Explore the iOS App Development Training Course
Free Demo: Register for a Course Demo
Official Website: Visit the JustAcademy Homepage